iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
AI & Data

雲端情人 - AI 愛系列 第 23

教育her-重構:用 WebhookParser 取代(不存在的)AsyncWebhookHandler

  • 分享至 

  • xImage
  •  

今天突然收到主辦方的站內訊通知, 今天要先連續PO2篇文今才可以繼續參賽,我今連緊急先寫2篇文章
https://ithelp.ithome.com.tw/upload/images/20250917/20112100FqrKZ609Ov.png

這2天修了好幾次翻譯模式和 linebotv3的bug過程大概步驟如下:


LINE Bot v3 沒有 AsyncWebhookHandler!改用 WebhookParser + 修好「翻譯模式」的兩個雷

今天把兩件長期踩雷的問題一次處理好:
1. 部署就炸: AsyncWebhookHandler 在 LINE Bot SDK v3 根本不存在。
2. 翻譯模式偶爾不動作: 開了「翻譯->英文」卻沒翻,或被其他指令攔截。

這篇會說明問題成因、最小修正步驟、完整路由思路,以及如何把「翻譯模式」變穩、變直覺。

為什麼 v3 會炸?

很多舊文章用的是 v2 的 WebhookHandler/裝飾器(甚至有人手滑打成 AsyncWebhookHandler)。但 v3 官方做法是用 WebhookParser 解析事件,然後自己把事件分派給對應的處理函式。

重點:別再從 linebot.v3.webhooks import AsyncWebhookHandler,它不存在。

最小可行修正(MVP)

  1. 初始化 Parser(取代 Handler)

from linebot.v3.webhooks import WebhookParser
from linebot.v3.messaging import Configuration, ApiClient, AsyncMessagingApi

configuration = Configuration(access_token=CHANNEL_TOKEN)
async_api_client = ApiClient(configuration=configuration)
line_bot_api = AsyncMessagingApi(api_client=async_api_client)

parser = WebhookParser(CHANNEL_SECRET) # ✅ 取代舊的 Handler

  1. FastAPI /callback:用 Parser 解析,再手動分派

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.webhooks import MessageEvent, TextMessageContent, AudioMessageContent, PostbackEvent

app = FastAPI()

@app.post("/callback")
async def callback(request: Request):
signature = request.headers.get("X-Line-Signature", "")
body = await request.body()
try:
events = parser.parse(body.decode("utf-8"), signature)
except InvalidSignatureError:
raise HTTPException(status_code=400, detail="Invalid signature")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Parse error: {e}")

# 依事件型別分派
for ev in events:
    if isinstance(ev, MessageEvent) and isinstance(ev.message, TextMessageContent):
        await on_text_message(ev)
    elif isinstance(ev, MessageEvent) and isinstance(ev.message, AudioMessageContent):
        await on_audio_message(ev)
    elif isinstance(ev, PostbackEvent):
        await on_postback(ev)

return JSONResponse({"status": "ok"})

翻譯模式為什麼會「看起來開了但沒翻」?

兩個常見坑:
1. 身分鍵不一致:v2 有 userId,有些 wrapper 會映射成 user_id;群組 groupId/group_id 也一樣。你如果把翻譯狀態存在 translation_states[chat_id],但 chat_id 算錯,就拿不到狀態。
2. 路由順序:翻譯模式開著時,輸入應該直接走翻譯,不要先被「股票、金價、選單」等分支攔截。

解法 A:統一取得 chat_id

def get_chat_id(event):
s = event.source
t = getattr(s, "type", "")
if t == "group":
return getattr(s, "groupId", None) or getattr(s, "group_id", None)
if t == "room":
return getattr(s, "roomId", None) or getattr(s, "room_id", None)
return getattr(s, "userId", None) or getattr(s, "user_id", None)

解法 B:把翻譯模式放到路由的「最高優先級」

translation_states: Dict[str, str] = {}

async def on_text_message(event):
chat_id = get_chat_id(event)
text = event.message.text.strip()
low = text.lower()

# 1) 切換翻譯模式
if low.startswith("翻譯->"):
    lang = text.split("->", 1)[1].strip()
    if lang == "結束":
        translation_states.pop(chat_id, None)
        return await reply("✅ 已結束翻譯模式")
    translation_states[chat_id] = lang
    return await reply(f"🌐 已開啟翻譯 → {lang},請直接輸入要翻的內容。")

# 2) ✅ 只要翻譯模式開著,就優先翻譯(**早於**股票/金價/選單等分支)
if chat_id in translation_states:
    out = await translate_text(text, translation_states[chat_id])
    return await reply(out)

# 3) 其他功能路由(股票、金價、選單…)
...

翻譯函式(純淨輸出)

LANGUAGE_MAP = {"英文":"English","日文":"Japanese","韓文":"Korean","越南文":"Vietnamese","繁體中文":"Traditional Chinese"}

async def translate_text(text: str, target_lang_display: str) -> str:
target = LANGUAGE_MAP.get(target_lang_display, target_lang_display)
sys = "You are a precise translation engine. Output ONLY the translated text with no extra words."
usr = f'{{"source_language":"auto","target_language":"{target}","text_to_translate":"{text}"}}'
return await groq_chat_async([{"role":"system","content":sys},{"role":"user","content":usr}], 800, 0.2)

小撇步:系統提示語加上 「only the translated text」,可以避免模型額外塞解釋與符號。

完整路由策略(你可以直接抄)
1. 翻譯->XXX:切換翻譯狀態(translation_states[chat_id] = "XXX")
2. **(優先)**若 chat_id 在翻譯狀態中 → 直接翻譯回覆
3. 其它指令路由:
• 「主選單」、「彩票分析」等 Postback
• 「金價」、「JPY」
• 股票/指數查詢(2330、NVDA、台股大盤、^GSPC…)
• 人設切換(甜/鹹/萌/酷/random)
• 一般聊天(人設 + 情緒)

驗收清單(5 分鐘)
• 輸入:翻譯->英文 → 回覆「已開啟翻譯 → 英文」
• 接著輸入中文句子 → 只回英文翻譯(沒有多餘字)
• 輸入:翻譯->結束 → 回覆「已結束翻譯模式」
• 群組裡若關閉自動回覆,必須 @bot 才有反應
• 其它功能(股票、金價、彩票)不受影響

常見坑位與對策
• ImportError: AsyncWebhookHandler 不存在 → 用 WebhookParser。
• 翻譯模式沒反應: 先檢查 chat_id 是否一致,再檢查路由順序。
• 回覆夾雜解釋文: 翻譯系統提示要明確要求「只輸出翻譯」。
• 群組吵: 增加「開啟/關閉自動回答」,關閉時需 @bot 才回。

結語

把 Handler 思維換成 Parser 思維,其實就兩步:解析 → 分派。同時把翻譯模式的「身分鍵」與「優先順序」處理好,就能回到「我說什麼你就翻什麼」的滑順手感。未來下一天 大概會把圖文與 Flex 子選單整理成可複用的模組化結構,讓加功能不再打結。


上一篇
Day 22:女友長虫囉:修bugs
下一篇
把服務撐起來-重構:健康檢查、監控、穩定度三寶(FastAPI × LINE SDK v3)
系列文
雲端情人 - AI 愛25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言